{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "w2wt2RJygcRR"
      },
      "source": [
        "# Learn OpenAI Whisper - Chapter 6\n",
        "## Notebook 2: Using Whisper for podcast transcription using RSS feeds\n",
        "\n",
        "This notebook provides a example of using FeedParser with OpenAI's Whisper.\n",
        "\n",
        "[FeedParser](https://feedparser.readthedocs.io/en/latest/) is a Python library for downloading and parsing syndicated feeds including RSS, Atom & RDF Feeds.\n",
        "\n",
        "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1RjVE2zPYY6FFAL3k0S4AgZJYmoV0uH6w)\n",
        "## Install required Python packages\n",
        "Run the cell below to install Whisper, FFMPEG (If not installed), and FeedParser.\n",
        "\n",
        "The Python libraries `openai`, `cohere`, and `tiktoken` are also installed because of dependencies for the `llmx` library. That is because `llmx` relies on them to function correctly."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "id": "fk9Jtws0xqLw"
      },
      "outputs": [],
      "source": [
        "%%capture\n",
        "!pip install -q cohere openai tiktoken\n",
        "!apt-get install ffmpeg\n",
        "!pip install -q \"git+https://github.com/openai/whisper.git\"\n",
        "!pip install -q feedparser requests"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {
        "id": "tI2em7TiLFFw"
      },
      "outputs": [],
      "source": [
        "import feedparser\n",
        "import requests\n",
        "import os\n",
        "import time\n",
        "from urllib.parse import urlparse\n",
        "import subprocess\n",
        "import re"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {
        "id": "-UlYl8ikXAsi"
      },
      "outputs": [],
      "source": [
        "def list_episodes(feed_url):\n",
        "    \"\"\"\n",
        "    Lists all episodes in the given RSS feed.\n",
        "\n",
        "    Parameters:\n",
        "    - feed_url: The RSS feed URL.\n",
        "\n",
        "    Returns:\n",
        "    - A list of tuples containing episode titles, their URLs and published dates in 'YYYYMMDD' format.\n",
        "    \"\"\"\n",
        "    d = feedparser.parse(feed_url)\n",
        "\n",
        "    episodes = []\n",
        "    for entry in d.entries:\n",
        "        title = entry.title\n",
        "        published = time.strftime('%Y%m%d', time.gmtime(time.mktime(entry.published_parsed)))\n",
        "        url = None\n",
        "        for link in entry.links:\n",
        "            if link.type == \"audio/mpeg\":\n",
        "                url = link.href\n",
        "                break\n",
        "        if url:\n",
        "            episodes.append((title, url, published))\n",
        "\n",
        "    return episodes\n",
        "\n",
        "from urllib.parse import urlparse\n",
        "\n",
        "def download_episode(url, filename=None):\n",
        "    \"\"\"\n",
        "    Downloads the podcast episode from the given URL.\n",
        "\n",
        "    Parameters:\n",
        "    - url: The URL of the podcast episode.\n",
        "    - filename: The desired filename to save the podcast. If not provided, it'll use the last part of the URL.\n",
        "\n",
        "    Returns:\n",
        "    - The path to the downloaded file.\n",
        "    \"\"\"\n",
        "    # If a custom filename is provided, append the appropriate extension from the URL\n",
        "    if filename:\n",
        "        parsed_url = urlparse(url)\n",
        "        # Extract only the base path without any query parameters\n",
        "        base_path = os.path.basename(parsed_url.path)\n",
        "        ext = os.path.splitext(base_path)[1]\n",
        "        filename += ext\n",
        "    else:\n",
        "        filename = os.path.basename(parsed_url.path)\n",
        "\n",
        "    response = requests.get(url, stream=True)\n",
        "    response.raise_for_status()\n",
        "\n",
        "    with open(filename, 'wb') as f:\n",
        "        for chunk in response.iter_content(chunk_size=8192):\n",
        "            f.write(chunk)\n",
        "\n",
        "    return filename\n",
        "\n",
        "def download_episode_start_end(url, filename=None, start_at=0, end_at=None):\n",
        "    \"\"\"\n",
        "    Downloads the podcast episode from the given URL and trims it starting from 'start_at' seconds\n",
        "    and ending at 'end_at' seconds.\n",
        "\n",
        "    Parameters:\n",
        "    - url: The URL of the podcast episode.\n",
        "    - filename: The desired filename to save the podcast. If not provided, it'll use the last part of the URL.\n",
        "    - start_at: The start time in seconds from where the audio should be trimmed.\n",
        "    - end_at: The end time in seconds up to which the audio should be trimmed. If not provided or set to 0, the audio will be trimmed till the end.    Returns:\n",
        "    - The path to the downloaded and trimmed file.\n",
        "    \"\"\"\n",
        "    parsed_url = urlparse(url)\n",
        "    if filename:\n",
        "        # Ensure the filename has the correct extension\n",
        "        ext = os.path.splitext(parsed_url.path)[1]\n",
        "        filename += ext\n",
        "    else:\n",
        "        filename = os.path.basename(parsed_url.path)\n",
        "\n",
        "    # Download the file\n",
        "    response = requests.get(url, stream=True)\n",
        "    response.raise_for_status()\n",
        "    temp_filename = \"temp_\" + filename\n",
        "    with open(temp_filename, 'wb') as f:\n",
        "        for chunk in response.iter_content(chunk_size=8192):\n",
        "            f.write(chunk)\n",
        "\n",
        "    # Use ffmpeg to trim the audio file\n",
        "    trimmed_filename = \"trimmed_\" + filename\n",
        "    command = ['ffmpeg', '-y', '-i', temp_filename, '-ss', str(start_at)]\n",
        "    if end_at is not None and end_at != 0:\n",
        "        command.extend(['-to', str(end_at)])\n",
        "    command.extend(['-c', 'copy', trimmed_filename])\n",
        "    subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
        "\n",
        "    # Remove the original downloaded file\n",
        "    os.remove(temp_filename)\n",
        "\n",
        "    return trimmed_filename"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fZk8grnzwVVJ"
      },
      "source": [
        "## RSS feed podcast"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "MGTC7X0CyyXQ",
        "outputId": "7da32a6e-ef36-46d7-f2de-db80fa45ec40"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Podcast name: Gigantes Podcast\n",
            "Number of episodes: 46\n",
            "Episodes:\n",
            "1. 20221011-La temporada vino, y se fue\n",
            "2. 20221004-Beisbol sin barreras con JP Martínez\n",
            "3. 20220927-El Cometa de Carolina\n",
            "4. 20220920-Es Septiembre y el roster lo sabe\n",
            "5. 20220913-El apagón\n",
            "6. 20220830-La ciencia detrás del juego con Saúl Martínez\n",
            "7. 20220809-Deje mi corazón en San Francisco\n",
            "8. 20220726-Yunior Marte, a young veteran\n",
            "9. 20220719-A la mitad del camino con las miras en alto\n",
            "10. 20220712-Sube y baja; Las opciones en el béisbol\n"
          ]
        }
      ],
      "source": [
        "# Gigantes Podcast Spanish\n",
        "podcast = 'http://feeds.megaphone.fm/ENTDM3955710136'\n",
        "# Science, Quickly\n",
        "# podcast = 'https://feeds.megaphone.fm/SAM8799470961'\n",
        "# WSJ Minute Briefing\n",
        "# podcast = 'https://feeds.megaphone.fm/WSJ7928321669'\n",
        "\n",
        "d = feedparser.parse(podcast)\n",
        "print(f\"Podcast name:\", d.feed.title)\n",
        "print(f\"Number of episodes:\", len(d.entries))\n",
        "\n",
        "# List episodes\n",
        "episodes = list_episodes(podcast)\n",
        "# Print the first 10 episodes\n",
        "print(\"Episodes:\")\n",
        "for idx, (title, url, published) in enumerate(episodes, 1):\n",
        "    print(f\"{idx}. {published}-{title}\")\n",
        "    if idx == 10:\n",
        "        break"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "metadata": {
        "id": "z0HtmCwoi8ho",
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "outputId": "b01f8288-9778-4af9-8d92-fdc58d99fbbf"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Downloaded 'La temporada vino, y se fue' as trimmed_20221011-La_temporada_vino_y_se_fue.mp3.\n"
          ]
        }
      ],
      "source": [
        "episode_num = 1 #@param {type:\"integer\"}\n",
        "drive_folder = \"\" #@param {type:\"string\"}\n",
        "start_at_seconds = 1300 #@param {type:\"integer\"}\n",
        "end_at_seconds = 0 #@param {type:\"integer\"}\n",
        "\n",
        "title, url, published = episodes[episode_num - 1]\n",
        "custom_filename = published + '-' + (re.sub('[^A-Za-z0-9 ]+', '', title[:75]).replace(' ', '_'))\n",
        "\n",
        "# Download the selected episode\n",
        "audio_file = download_episode_start_end(url, drive_folder + custom_filename, start_at_seconds, end_at_seconds)\n",
        "print(f\"Downloaded '{title}' as {audio_file}.\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 79,
          "referenced_widgets": [
            "77c3fe204e3c4b1d8aa5d1da66de7533",
            "ab444a8000bf43a8ae9ec3a87cf06d84"
          ]
        },
        "id": "SA1LrZaHbwKs",
        "outputId": "84c4a997-0f9c-4a25-a8c3-7dc59fc1c214"
      },
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "Audio(value=b'ID3\\x04\\x00\\x00\\x00\\x00\\x06ETIT2\\x00\\x00\\x00\\x1d\\x00\\x00\\x03La temporada vino, y se fue\\x00TXXX\\…"
            ],
            "application/vnd.jupyter.widget-view+json": {
              "version_major": 2,
              "version_minor": 0,
              "model_id": "77c3fe204e3c4b1d8aa5d1da66de7533"
            }
          },
          "metadata": {}
        }
      ],
      "source": [
        "import ipywidgets as widgets\n",
        "widgets.Audio.from_file(audio_file, autoplay=False, loop=False)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "kbiYt0SG0qKE",
        "outputId": "cb663356-c9b6-42e5-a1d9-3b34f9ae33cb"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "[nltk_data] Downloading package punkt to /root/nltk_data...\n",
            "[nltk_data]   Unzipping tokenizers/punkt.zip.\n",
            "100%|███████████████████████████████████████| 461M/461M [00:08<00:00, 57.6MiB/s]\n"
          ]
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Detected language: es\n",
            "----\n",
            "Transcription from audio:\n",
            "y el bullpen estuviese en una mejor posición y yo entiendo que mirando hacia el futuro vamos a ver cómo vamos a tener alternativa de una manera positiva para que el equipo cuando sufra esas lesiones aún estén en una buena posición.\n",
            "Y qué bueno que mencionas eso del Open Air, cómo hacerle quizás la vida más simple a los jovencitos que vienen porque muchos piensan que el usar el Open Air es por diseño, pero no es por necesidad que los equipos en ocasiones\n",
            "----\n",
            "Translation from audio:\n",
            "The bullpen is in a better position and I understand that looking at the future we will see how we will have an alternative in a positive way so that the team, when suffering those injuries, is still in a good position.\n",
            "And how good is that you mention the opener, how to make perhaps the simplest life for the young people who come because many think that using the opener is for design, but it is not because of the need that the teams on occasion\n"
          ]
        }
      ],
      "source": [
        "import whisper\n",
        "import torch\n",
        "# NLTK helps to split the transcription sentence by sentence\n",
        "import nltk\n",
        "nltk.download('punkt')\n",
        "from nltk import sent_tokenize\n",
        "\n",
        "model = whisper.load_model(\"small\")\n",
        "\n",
        "audio = whisper.load_audio(audio_file)\n",
        "audio = whisper.pad_or_trim(audio)\n",
        "\n",
        "# make log-Mel spectrogram and move to the same device as the model\n",
        "mel = whisper.log_mel_spectrogram(audio).to(model.device)\n",
        "\n",
        "# detect the spoken language\n",
        "_, probs = model.detect_language(mel)\n",
        "audio_lang = max(probs, key=probs.get)\n",
        "print(f\"Detected language: {audio_lang}\")\n",
        "\n",
        "# decode the audio\n",
        "options = whisper.DecodingOptions(fp16=torch.cuda.is_available(), language=audio_lang, task='transcribe')\n",
        "result = whisper.decode(model, mel, options)\n",
        "\n",
        "# print the recognized text\n",
        "print(\"----\\nTranscription from audio:\")\n",
        "for sent in sent_tokenize(result.text):\n",
        "  print(sent)\n",
        "\n",
        "# decode the audio\n",
        "options = whisper.DecodingOptions(fp16=torch.cuda.is_available(), language=audio_lang, task='translate')\n",
        "result = whisper.decode(model, mel, options)\n",
        "\n",
        "# print the recognized text\n",
        "print(\"----\\nTranslation from audio:\")\n",
        "for sent in sent_tokenize(result.text):\n",
        "  print(sent)"
      ]
    }
  ],
  "metadata": {
    "accelerator": "GPU",
    "colab": {
      "gpuType": "T4",
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    },
    "language_info": {
      "name": "python"
    },
    "widgets": {
      "application/vnd.jupyter.widget-state+json": {
        "77c3fe204e3c4b1d8aa5d1da66de7533": {
          "model_module": "@jupyter-widgets/controls",
          "model_name": "AudioModel",
          "model_module_version": "1.5.0",
          "state": {
            "_dom_classes": [],
            "_model_module": "@jupyter-widgets/controls",
            "_model_module_version": "1.5.0",
            "_model_name": "AudioModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/controls",
            "_view_module_version": "1.5.0",
            "_view_name": "AudioView",
            "autoplay": false,
            "controls": true,
            "format": "mpeg",
            "layout": "IPY_MODEL_ab444a8000bf43a8ae9ec3a87cf06d84",
            "loop": false
          }
        },
        "ab444a8000bf43a8ae9ec3a87cf06d84": {
          "model_module": "@jupyter-widgets/base",
          "model_name": "LayoutModel",
          "model_module_version": "1.2.0",
          "state": {
            "_model_module": "@jupyter-widgets/base",
            "_model_module_version": "1.2.0",
            "_model_name": "LayoutModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/base",
            "_view_module_version": "1.2.0",
            "_view_name": "LayoutView",
            "align_content": null,
            "align_items": null,
            "align_self": null,
            "border": null,
            "bottom": null,
            "display": null,
            "flex": null,
            "flex_flow": null,
            "grid_area": null,
            "grid_auto_columns": null,
            "grid_auto_flow": null,
            "grid_auto_rows": null,
            "grid_column": null,
            "grid_gap": null,
            "grid_row": null,
            "grid_template_areas": null,
            "grid_template_columns": null,
            "grid_template_rows": null,
            "height": null,
            "justify_content": null,
            "justify_items": null,
            "left": null,
            "margin": null,
            "max_height": null,
            "max_width": null,
            "min_height": null,
            "min_width": null,
            "object_fit": null,
            "object_position": null,
            "order": null,
            "overflow": null,
            "overflow_x": null,
            "overflow_y": null,
            "padding": null,
            "right": null,
            "top": null,
            "visibility": null,
            "width": null
          }
        }
      }
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}